<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Toetem Referee - Chapter 11</title>
	<link rel="stylesheet" href="/strophe/public/css/jquery-ui-1.8.21.custom.css">
	<link rel="stylesheet" href="/strophe/public/css/toetem/referee.css">
	<script type="text/javascript" src="/strophe/public/js/jquery-1.7.2.min.js"></script>
	<script type="text/javascript" src="/strophe/public/js/jquery-ui-1.8.21.custom.min.js"></script>
	<script type="text/javascript" src="/strophe/public/js/strophe.js"></script>
	<script type="text/javascript" src="/strophe/public/js/flXHR.js"></script>
	<script type="text/javascript" src="/strophe/public/js/strophe.flxhr.js"></script>
	<script type="text/javascript" src="/strophe/public/js/toetem/board.js"></script>
	<script type="text/javascript">
		var Referee = {
			connection: null,
			waiting: [],
			presence: {},
			games: {},
			NS_TOETEM: "http://muchine.net/ns/toetem",
			NS_MUC: "http://jabber.org/protocol/muc",
			NS_MUC_USER: "http://jabber.org/protocol/muc#user",
			NS_MUC_OWNER: "http://jabber.org/protocol/muc#owner",
			MUC_SERVICE: "conference.openfire",
			
			onIq: function(iq) {
				var id = $(iq).attr('id');
				var from = $(iq).attr('from');
				var type = $(iq).attr('type');
				
				Referee.log("iq received from " + from);
				
				// make sure we know the user's presence first
				var bareFrom = Strophe.getBareJidFromJid(from);
				if (!Referee.presence[bareFrom]) {
					Referee.sendError(iq, 'auth', 'forbidden');
					Referee.log('unkown user presence. from: ' + bareFrom);
					return true;
				}
				
				// check if iq includes toetem namspace.
				var child = $(iq).find('*[xmlns="' + Referee.NS_TOETEM + '"]:first');
				if (child.length == 0) {
					Referee.sendError(iq, 'cancel', 'feature-not-implemented');
					Referee.log("unknown namespace");
					return true;
				}
				
				// check if iq type is valid. At this point of time, only set type is allowed.
				if (type == "get") {
					Referee.sendError(iq, 'cancel', 'bad-request');
					return true;
				}
				else if (type != "set") {
					// ignore IQ-error and IQ-result
					return true;
				}
				
				// analyze the command
				switch (child[0].tagName) {
					case "waiting":
						Referee.onWaiting(id, from, child);
						break;
					case "stop-waiting":
						Referee.onStopWaiting(id, from, child);
						break;
					case "start":
						Referee.onGameStart(iq, id, from, child);
						break;
					case "resign":
						Referee.onResign(iq, id, from);
						break;
					default:
						Referee.sendError(iq, 'cancel', 'bad-request');
				}
				
				return true;
			},
			
			onPresence: function(pres) {
				var from = $(pres).attr('from');
				var bareFrom = Strophe.getBareJidFromJid(from);
				var type = $(pres).attr('type');
				var bareJid = Strophe.getBareJidFromJid(Referee.connection.jid);
				var domain = Strophe.getDomainFromJid(from);
				
				if (domain == Referee.MUC_SERVICE) {
					// handle room presence
					var room = Strophe.getNodeFromJid(from);
					var player = Strophe.getResourceFromJid(from);
					var game = Referee.games[room];
					
					Referee.log("MUC presence received. room: " + room + ", player: " + player);
					
					// make sure it's a game and player we care about
					// QUESTION: resource of from attribute in presence stanza is full jid of player?
					if (game && (game.status == "starting" || game.status == "playing") &&
							(player == game.xPlayer || player == game.oPlayer)) {
						if (game.status == "starting") {
							if (type != "unavailable") {
								// waiting for one less player; if both are now present, the game is started
								game.waiting -= 1;
								
								Referee.log("Player " + player + " arrived to game " + game.room + ".");
								if (game.waiting == 0) {
									Referee.startGame(game);
								}
							}
							else {
								// one of the players left before the game even started, so abort the game
								Referee.endGame(game, 'aborted');
							}
						}
						else {
							// during play, forfeit a player if they leave the room
							if (type == "unavailable") {
								if (player == game.xPlayer) {
									game.winner = game.oPlayer;
								}
								else {
									game.winner = game.xPlayer;
								}
								
								Referee.endGame(game, 'finished');
							}
						}
					}
				}
				else if ((!type || type ==  "unavailable") && from != bareJid) {
					// only look for direct presence from other users
					if (type == "unavailable") {
						delete Referee.presence[bareFrom];
						Referee.log("Unregistered " + bareFrom + ".");
						
						// remove from lists
						Referee.removeWaiting(from);
					}
					else if ($(pres).find('register').length > 0) {
						Referee.presence[bareFrom] = from;
						Referee.log("Registered " + bareFrom + ".");
												
						// send waiting list to registered user
						Referee.sendWaiting(from);
						Referee.sendGames(from);
					}
				}
				
				return true;
			},
			
			onGameStart: function(iq, id, from, element) {
				var withJid = $(element).attr('with');
				var withBareJid = Strophe.getBareJidFromJid(withJid);
				
				Referee.log("Game start requested is being processed. with: " + withJid);
				
				// check that the players are available
				if (!Referee.isWaiting(withJid)) {
					Referee.sendError(iq, 'modify', 'item-not-found');
					return;
				}
				
				if (Referee.isPlaying(withJid) || Referee.isPlaying(from)) {
					Referee.sendError(iq, 'cancel', 'not-allowed');
					return;
				}
				
				Referee.connection.send($iq({to:from, id:id, type: "result"}));
				
				// remove players from waiting list
				Referee.removeWaiting(from);
				Referee.removeWaiting(withJid);
				
				// create game room and invite players
				Referee.createGame(from, withJid);
			},
			
			createGame: function(player1, player2) {
				// generate a random room name, and make sure it doesn't already exist to our knowledge
				var room;
				do {
					room = "" + Math.floor(Math.random() * 1000000);
				} while(Referee.games[room]);
				
				Referee.log("create game... room: " + room);
				
				var roomJid = Referee.getGameRoom(room) + "/referee";
				Referee.connection.addHandler(function(presence) {
					var game;
					
					if ($(presence).find('status[code="201"]').length > 0) {
						Referee.log("room was successfully created.");
						Referee.log("player1: " + player1 + ", player2: " + player2);
						
						// room was freshly created
						game = Referee.newGame();
						game.room = room;
						
						// create initial game state with randomized sides
						if (Math.random() < 0.5) {
							game.xPlayer = player1;
							game.oPlayer = player2;
						}
						else {
							game.xPlayer = player2;
							game.oPlayer = player1;
						}
						
						Referee.games[room] = game;
						
						// invite players to start the game
						Referee.invitePlayers(game);
						
						Referee.log("broadcast new room info.");
						
						// notify everyone about the game
						Referee.broadcast(function(msg) {
							return msg
								.c('games', {xmlns: Referee.NS_TOETEM})
									.c('game', {'x-player': game.xPlayer, 'o-player': game.oPlayer, room: Referee.getGameRoom(room)});
						});
						
						Referee.log("Created game room " + room + ".");
					}
					else {
						// room was already in use, we need to start over
						Referee.connection.send($pres({to:roomJid, type: "unavailable"}));
						Referee.createGame(player1, player2);
					}
					
					return false;
				}, null, "presence", null, null, roomJid);
				
				Referee.log("send room creation request. room jid: " + roomJid);
				Referee.connection.send($pres({to: roomJid}).c("x", {xmlns: Referee.NS_MUC}));
			},
			
			newGame: function() {
				return {
					room: null,
					waiting: 2,
					status: "starting",
					xPlayer: null,
					oPlayer: null,
					winner: null
				};
			},
			
			sendGames: function(jid) {
				var message = 
					$msg({to: jid})
						.c('games', {xmlns: Referee.NS_TOETEM});
				
				$.each(Referee.games, function(game) {
					message.c('game', {"x-player": this.xPlayer, "o-player": this.oPlayer, room: Referee.getGameRoom(this.room)}).up();
				});
				
				Referee.connection.send(message);
			},
			
			startGame: function(game) {
				game.status = "playing";
				
				Referee.log("start game...");
				
				Referee.connection.send(
					Referee.getMucMessage(game)
						.c('body').t('The match has started.').up()
						.c('game-started', {xmlns: Referee.NS_TOETEM, 'x-player': game.xPlayer, 'o-player': game.oPlayer})
				);
				
				Referee.log("started game.");
				
				Referee.log("Started game " + game.room + ".");
			},
			
			endGame: function(game, status) {
				game.status = status;
				
				// let room know the reuslt of the game
				var attrs = {xmlns: Referee.NS_TOETEM};
				if (game.winner) {
					attrs.winner = game.winner;
				}
				
				var message = "";
				if (game.winner) {
					message += Strophe.getBareJidFromJid(game.winner) + " has won the match.";
				}
				else if (status == 'finished') {
					message += "The match was tied.";
				}
				else {
					message += "The match was aborted.";
				}
				
				Referee.connection.send(
					Referee.getMucMessage(game)
						.c('body').t(message).up()
						.c('game-ended', attrs)
				);
				
				// delete the game
				delete Referee.games[game.room];
				
				// leave the room
				Referee.connection.send($pres({to: Referee.getGameRoom(game.room) + "/referee", type: "unavailable"}));
				
				// notify all the players
				Referee.broadcast(function(message) {
					return message
						.c('game-over', {xmlns: Referee.NS_TOETEM})
						.c('game', {'x-player': game.xPlayer, 'o-player': game.oPlayer, room: Referee.getGameRoom(game.room)})
				});
				
				Referee.log("Finished game " + game.room + ".");
			},
			
			findGame: function(player) {
				var game = null;
				$.each(Referee.games, function(item) {
					if (item.xPlayer == player || item.oPlayer == player) {
						game = item;
						return false;
					}
				});
				
				return game;
			},
			
			getGameRoom: function(room) {
				return room + "@" + Referee.MUC_SERVICE;
			},
			
			getMucMessage: function(game) {
				return $msg({to: Referee.getGameRoom(game.room), type: "groupchat"});
			},
			
			invitePlayers: function(game) {
				Referee.log("invite players: " + game.xPlayer + ", " + game.oPlayer);
				
				// send room invites
				$.each([game.xPlayer, game.oPlayer], function() {
					Referee.connection.send(
						$msg({to: Referee.getGameRoom(game.room)})
							.c("x", {xmlns: Referee.NS_MUC_USER})
								.c("invite", {to: this})
					);
				});
			},
			
			onResign: function(iq, id, from) {
				var game = Referee.findGame(from);
				if (!game || game.status != "playing") {
					Referee.sendError(iq, "cancel", "bad-request");
				}
				else {
					if (from == game.xPlayer) {
						game.winner = game.oPlayer;
					}
					else {
						game.winner = game.xPlayer;
					}
					
					Referee.endGame(game, 'finished');
					
					// respond to resign request
					Referee.connection.send($iq({to: from, id: id, type: "result"}));
					
					Referee.log(Strophe.getBareJidFromJid(from) + " resigned game " + game.room + ".");
				}
			},
			
			onWaiting: function(id, from, element) {
				// if they were already waiting, remove them so their resource can be updated.
				if (Referee.isWaiting(from)) {
					Referee.removeWaiting(from);
				}
				
				Referee.waiting.push(from);
				
				Referee.connection.send($iq({to: from, id: id, type: "result"}));
				Referee.broadcast(function(msg) {
					return msg
						.c('waiting', {xmlns: Referee.NS_TOETEM})
							.c('player', {jid: from});
				});
				
				Referee.log("Added " + Strophe.getBareJidFromJid(from) + " to waiting list.");
			},
			
			onStopWaiting: function(id, from, element) {
				if (!Referee.isWaiting(from)) {
					// TODO: send bad request error
				}
				
				Referee.removeWaiting(from);
				
				Referee.connection.send($iq({to: from, id: id, type: "result"}));
				Referee.broadcast(function(msg) {
					return msg
						.c('not-waiting', {xmlns: Referee.NS_TOETEM})
							.c('player', {jid: from});
				});
			},
			
			removeWaiting: function(jid) {
				var bareJid = Strophe.getBareJidFromJid(jid);
				
				// check if the given jid exists in the waiting list
				// NOTE: waiting list stores full jid, but comparison is performed with bare jid
				// 		 So, even if the resources are different it is considered the user is in the waiting list.
				var i;
				for (i = 0; i < Referee.waiting.length; i++) {
					var waitJid = Strophe.getBareJidFromJid(Referee.waiting[i]);
					if (waitJid == bareJid) {
						break;
					}
				}
				
				// if exists in the waiting list, remove it and broadcast a change to others
				if (i < Referee.waiting.length) {
					Referee.waiting.splice(i, 1);
					
					Referee.broadcast(function(msg) {
						return msg
							.c('not-waiting', {xmlns: Referee.NS_TOETEM})
								.c('player', {jid: jid});
					});
				}
				
				Referee.log("Removed " + bareJid + " from waiting list.");
			},
			
			sendWaiting: function(jid) {
				var message = 
					$msg({to: jid})
						.c('waiting', {xmlns: Referee.NS_TOETEM});
					
				for (var i = 0; i < Referee.waiting.length; i++) {
					message.c('player', {jid: Referee.waiting[i]}).up();
				}
				
				Referee.connection.send(message);
			},
			
			isWaiting: function(jid) {
				// compare bare jid, not full jid
				var bareJid = Strophe.getBareJidFromJid(jid);
				
				for (var i = 0; i < Referee.waiting.length; i++) {
					var waitJid = Strophe.getBareJidFromJid(Referee.waiting[i]);
					if (waitJid == bareJid) {
						return true;
					}
				}
				
				return false;
			},
			
			isPlaying: function(jid) {
				var bareJid = Strophe.getBareJidFromJid(jid);
				
				var found = false;
				$.each(Referee.games, function() {
					xPlayer = Strophe.getBareJidFromJid(this.xPlayer);
					oPlayer = Strophe.getBareJidFromJid(this.oPlayer);
					if (xPlayer == bareJid || oPlayer == bareJid) {
						found = true;
						return false;
					}
				});
				
				return found;
			},
			
			broadcast: function(func) {
				$.each(Referee.presence, function() {
					var msg = func($msg({to: this}));
					Referee.connection.send(msg);
				});
			},
			
			sendError: function(iq, etype, ename, appError) {
				var error = 
						$iq({to: $(iq).attr('from'), id: $(iq).attr('id'), type: "error"})
							.cnode(iq.cloneNode(true)).up()
							.c('error', {type: etype})
								.c('ename', {xmlns: Strophe.NS.STANZAS}).up();
				
				if (appError) {
					error.c(appError, {xmlns: Referee.NS_TOETEM});
				}
				
				Refereeh.connection.send(error);
			},
			
			log: function(message) {
				$('#log').prepend("<p>" + message + "</p>");
			}
		};
		
		$(document).ready(function() {
			$('#login_dialog').dialog({
				autoOpen: true,
				draggable: false,
				modal: true,
				title: 'Connect to XMPP',
				buttons: {
					"Connect": function() {
						$(document).trigger('connect', {
							jid: $('#jid').val(),
							password: $('#password').val()
						});
						
// 						$('#password').val('');
						$(this).dialog('close');
					}					
				}
			});
			
			$('#disconnect').click(function() {
				Referee.connection.disconnect();
			});
		});
		
		$(document).bind('connect', function(ev, data) {
			var conn = new Strophe.Connection("${config.boshUrl}");
			conn.connect(data.jid, data.password, function(status) {
				if (status == Strophe.Status.CONNECTED) {
					$(document).trigger('connected');
				}
				else if (status == Strophe.Status.DISCONNECTED) {
					$(document).trigger('disconnected');
				}
			});
			
			Referee.connection = conn;
		});
		
		$(document).bind('connected', function() {
			$('#disconnect').removeAttr('disabled');
			
			var conn = Referee.connection;
			$('#log').prepend("<p>Connected as " + conn.jid + "</p>");
			
			conn.addHandler(Referee.onPresence, null, "presence");
			conn.addHandler(Referee.onIq, null, "iq");
			
			conn.send($pres());
		});
		
		$(document).bind('disconnected', function() {
			$('#disconnect').attr('disabled', 'disabled');
			Referee.log("disconnected.");
		});
	</script>
</head>
<body>
	<h1>Toetem Referee</h1>
	
	<div class='buttons'>
		<input type='button' id='disconnect' value='disconnect' class='button' disabled='disabled'>	
	</div>
	
	<div id='log'></div>
	
	<!-- login_dialog -->
	<div id='login_dialog' class='hidden'>
		<label>JID:</label><input type='text' id='jid' value='referee@openfire/referee'><br>
		<label>Password:</label><input type='password' id='password' value='welcome1'>
	</div>
</body>
</html>